package com.cybertron.ironhide.manage.common.jdbc.plugin;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.DefaultParameterHandler;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.MappedStatement.Builder;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.log4j.Logger;

import com.cybertron.ironhide.manage.common.base.Pager;
import com.cybertron.ironhide.manage.common.jdbc.dialect.Dialect;
import com.cybertron.ironhide.manage.common.jdbc.dialect.MySQLDialect;

@Intercepts({ @Signature(type = Executor.class, method = "query", args = {
		MappedStatement.class, Object.class, RowBounds.class,
		ResultHandler.class }) })
public class PaginationJdbcInterceptor implements Interceptor {

	private final static Logger logger = Logger
			.getLogger(PaginationJdbcInterceptor.class);

	private Dialect dialect = new MySQLDialect();

	public Object intercept(Invocation invocation) throws Throwable {

		MappedStatement mappedStatement = (MappedStatement) invocation
				.getArgs()[0];
		Object parameter = invocation.getArgs()[1];
		RowBounds bounds = (RowBounds) invocation.getArgs()[2];
		if (!(bounds instanceof Pager)) {
			return invocation.proceed();
		}
		Pager pager = (Pager) bounds;
		Integer offset = pager.getPageNum();
		Integer limit = pager.getNumPerPage();

		BoundSql boundSql = mappedStatement.getBoundSql(parameter);
		String originalSql = boundSql.getSql().trim();
		Configuration configuration = mappedStatement.getConfiguration();
		if (boundSql == null || boundSql.getSql() == null
				|| "".equals(boundSql.getSql())) {
			return null;
		}

		String countSql = dialect.getCountString(originalSql);

		Connection connection = configuration.getEnvironment().getDataSource()
				.getConnection();
		PreparedStatement countStmt = connection.prepareStatement(countSql);

		ParameterHandler parameterHandler = new DefaultParameterHandler(
				mappedStatement, boundSql.getParameterObject(),
				getPageBoundSql(boundSql, countSql));
		parameterHandler.setParameters(countStmt);

		ResultSet rs = countStmt.executeQuery();

		if (rs.next()) {
			pager.setTotalCount(rs.getLong(1));
		}
		rs.close();
		countStmt.close();
		connection.close();

		if (logger.isDebugEnabled()) {
			logger.debug("executed count sql");
		}

		String pageSql = dialect.getLimitString(originalSql, offset, limit);

		MappedStatement newMs = copyFromMappedStatement(mappedStatement,
				new PageSqlSource(getPageBoundSql(boundSql, pageSql)));

		invocation.getArgs()[0] = newMs;
		return invocation.proceed();
	}

	private class PageSqlSource implements SqlSource {

		private BoundSql boundSql;

		public PageSqlSource(BoundSql boundSql) {
			super();
			this.boundSql = boundSql;
		}

		@Override
		public BoundSql getBoundSql(Object parameterObject) {
			return boundSql;
		}

	}

	public Object plugin(Object arg0) {
		return Plugin.wrap(arg0, this);
	}

	public void setProperties(Properties arg0) {

	}

	private BoundSql getPageBoundSql(BoundSql boundSql, String sql) {
		Field field;
		try {
			field = boundSql.getClass().getDeclaredField("sql");
			field.setAccessible(true);
			field.set(boundSql, sql);
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchFieldException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
		return boundSql;
	}

	private MappedStatement copyFromMappedStatement(MappedStatement ms,
			SqlSource sqlSource) {
		Builder builder = new MappedStatement.Builder(ms.getConfiguration(),
				ms.getId(), sqlSource, ms.getSqlCommandType());
		builder.resource(ms.getResource());
		builder.fetchSize(ms.getFetchSize());
		builder.statementType(ms.getStatementType());
		builder.keyGenerator(ms.getKeyGenerator());
		builder.timeout(ms.getTimeout());
		builder.parameterMap(ms.getParameterMap());
		builder.resultMaps(ms.getResultMaps());
		builder.cache(ms.getCache());
		MappedStatement newMs = builder.build();
		return newMs;
	}

}